iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 22
0

寬鬆相等 vs. 嚴格相等

嚴格和寬鬆他們之間最重要的差異就是如何判斷相等。
普遍的誤解:

  • ==:檢查值的相等性。
  • ===:檢查值與型別的相等性。

但正確的應該是:

  • ==:允許相等性比較中的強制轉型。
  • ===:不允許強制轉型。

相等性的效能

思考一下誤解觀念和正確觀念之間的差異。
誤解的想法:嚴格比較似乎做比較多事。
正確的想法:寬鬆比較才是做比較多事,因為型別不同還需要依照步驟強制轉型。

而這在效能的比較只有微秒的差異,選用運算子應該判斷當下是否需要強制轉型。

抽象相等性

ECMA 5 11.9.3.1

  • 被比較的兩個值具有相同型別,他們就會以同一性來進行比較。
    例外:
    • NaN 永遠不同等於自己
    • +0 與 -0 彼此相等
  • 物件之間的寬鬆比較:兩個值只會在兩個都指向完全相同值的參考時才會相等,這邊不會發生強制轉型。
  • 使用寬鬆比較兩個值,需要將其中一個值或兩個值隱含地強制轉型,讓兩個值都具有同一型別。

比較:字串與數字

var a = 42;
var b = "42";

a === b;	// false
a == b;		// true

嚴格等於不會強制轉型,所以第一組是 false,
那在寬鬆等於這邊是怎麼運算呢?

ECMA 5 11.9.3
如果 Type(x) 是 Number 而 Type(y) 是 String, 返回比较 x == ToNumber(y)的结果。
如果 Type(x) 是 String 而 Type(y) 是 Number, 返回比较 ToNumber(x) == y的结果。

所以第二組相等被強制轉型成數字做運算,就是ToNumber的抽象運算。

比較:boolean 與其他值

var a = "42";
var b = true;

a == b;	// false

而 42 是 truthy 的值,為何結果會 false 呢?

ECMA 5 11.9.3
如果 Type(x) 是Boolean, 返回比较 ToNumber(x) == y 的结果。
如果 Type(y) 是Boolean, 返回比较 x == ToNumber(y) 的结果。

轉型過程是這樣:

  • true 根據上面規格被轉型為1
  • '42' 理所當然被轉型為 42
  • 42 不等於 1

而 false 理所當然被轉為0

永遠不要用 == true== false

可以嘗試以下判斷:

var a = "42"; 

// 不好(會失敗的!): 
if (a == true) { // .. } 

// 也不該(會失敗的!):
if (a === true) { // .. }

// 足夠好(隱含地工作):
if (a) { // .. }

// 更好(明確地工作):
if (!!a) { // .. } 

// 也很好(明確地工作):
if (Boolean( a )) { // .. }

比較:null 與 undefined

ECMA 5 11.9.3
如果 x 是 null 而 y 是 undefined,返回 true。
如果 x 是 undefined 而 y 是 null,返回 true。

所以 nullundefined 在寬鬆相等會等於彼此,但不會等於整個語言的其他值。

var a = null;
var b;

a == b;		// true
a == null;	// true
b == null;	// true

a == false;	// false
b == false;	// false
a == "";	// false
b == "";	// false
a == 0;		// false
b == 0;		// false

null 和 undefined 之間的強制轉型是安全的。

var a = doSomething();

if (a == null) {
	// ..
}

這樣 a 除了 null 和 undefined ,就算是其他 falsy 值都不會過。

比較:物件與非物件

ECMA 5 11.9.3
如果 Type(x) 是一个 String 或者 Number 而 Type(y) 是一个Object, 返回比较 x == ToPrimitive(y) 的结果。
如果 Type(x) 是一个 Object 而 Type(y) 是 String 或者 Number, 返回比较 ToPrimitive(x) == y 的结果。

沒有布林是因為布林會先被強制轉型為數字

var a = 42;
var b = [ 42 ];

a == b;	// true

步驟:

  • b 會呼叫 ToPrimitive
  • b 會變成'42'
  • 轉型為數字
  • 相等

解封裝與 ToPrimitive 強制轉型有關:

var a = "abc";
var b = Object( a );	// 與`new String( a )`相同

a === b;				// false
a == b;					// true

a == b 是 true,因為經過 ToPrimitive 強制轉型。

解封裝就是解開基本型別值周圍的包裹器,回傳底層的基底值。
例如new String( a ) 回傳 abc

== 有其他優先的規則:

var a = null;
var b = Object( a );	// 與`Object()`相同
a == b;			// false

var c = undefined;
var d = Object( c );	// 與`Object()`相同
c == d;			// false

var e = NaN;
var f = Object( e );	// 與`new Number( e )`相同
e == f;			// false

null 和 undefined 無法被封裝,沒有對應的物件包裹器,所以一樣會產生一個普通的物件,而 NaN 可封裝他的 Number 包裹器,但在 == 解封後會失敗,因為 NaN 永遠不與自己相等。

邊緣情況

檢視修改內建的原生原型會產生什麼結果。

有任何其他值可能會⋯⋯

Number.prototype.valueOf = function() {
	return 3;
};

new Number( 2 ) == 3;	// true

因為 2 或 3 兩個都是基本型別值,所以可以直接比較,並不會用到 valueOf,但new Number( 2 )必須經過 ToPrimitive 強制轉型,因此會呼叫valueOf

if (a == 2 && a == 3) {
	// ..
}

...不會有變數等於 2 又等於 3,所以這段程式沒有任何意義。

var i = 2;

Number.prototype.valueOf = function() {
	return i++;
};

var a = new Number( 42 );

if (a == 2 && a == 3) {
	console.log( "Yep, this happened." );
}

這段程式碼有 side-effect,每次產生的結果都不相同。

Falsy 的比較

列出 Falsy 的比較:

"0" == null;			// false
"0" == undefined;		// false
"0" == false;			// true -- 噢!
"0" == NaN;				// false
"0" == 0;				// true
"0" == "";				// false

false == null;			// false
false == undefined;		// false
false == NaN;			// false
false == 0;				// true -- 噢!
false == "";			// true -- 噢!
false == [];			// true -- 噢!
false == {};			// false

"" == null;				// false
"" == undefined;		// false
"" == NaN;				// false
"" == 0;				// true -- 噢!
"" == [];				// true -- 噢!
"" == {};				// false

0 == null;				// false
0 == undefined;			// false
0 == NaN;				// false
0 == [];				// true -- 噢!
0 == {};				// false
  • """NaN"是根本不可能相等的值。
  • "0"0是合理相等的。

瘋狂的例子

[] == ![];		// true
  • ! 運算子優先性大於==
  • 寬鬆相等強制轉型
  • [] == Boolean(![])
  • '' == false
  • 0 == 0
  • true
2 == [2];		// true
"" == [null];	// true
  • 右手邊的[2][null]會經過 ToPrimitive 的強制轉型
  • 2 == '2''' == ''
0 == "\n";		// true

經由 ToNumber 轉型後 "\n" 會等於""

42 == "43";			// false
"foo" == 42;			// false
//NaN == 42
"true" == true;		// false
//NaN == 1

42 == "42";			// true
"foo" == [ "foo" ];	        // true

合理性的檢查

"0" == false;			// true -- 噢!
false == 0;				// true -- 噢!
false == "";			// true -- 噢!
false == [];			// true -- 噢!
"" == 0;				// true -- 噢!
"" == [];				// true -- 噢!
0 == [];				// true -- 噢!

扣除掉永遠都該避免使用的東西後:

"" == 0;				// true -- 噢!
"" == [];				// true -- 噢!
0 == [];				// true -- 噢!

安全地使用隱含的強制轉型

  • 一邊 true 或 false ,千萬不要使用 ==
  • 任一邊有可能[]""或 0 ,就考慮不要用==

JavaScript-Equality-Table

抽象的關係式比較

var a = [ 42 ];
var b = [ "43" ];

a < b;	// true
b < a;	// false
  • 呼叫 ToPrimitive
  • 任一回傳不是字串,就會轉型為數字
var a = [ "42" ];
var b = [ "043" ];

a < b;	// false

ToPrimitive 轉型後皆為字串,如果兩者都是字串,就會開始逐字比較。

同樣邏輯也適用於:

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];

a < b;	// false

此範例也為逐字比較。

var a = { b: 42 };
var b = { b: 43 };

a < b;	// ?

兩個皆為[object Object],在辭典順序 a 沒有小於 b。

var a = { b: 42 };
var b = { b: 43 };

a < b;	// false
a == b;	// false
a > b;	// false

a <= b;	// true
a >= b;	// true
  • a 與 b 不相等是因為他們不是同一個參考。
  • a <= b; 先估算 b < a 再否定其結果,所以 false 的反向是 true。
var a = [ 42 ];
var b = "043";

a < b;						// false -- 字串比较!
Number( a ) < Number( b );	// true -- 數字比较!

上一篇
Day21:YDKJS 第五次讀書會(上)
下一篇
Day23:JavaScript 設計模式 優良部份 Chapter03 物件 筆記精要
系列文
寇丁人妻的前端書蟲日誌30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言